/*
1. transect class to represents a linear transect.
2. dataStore class to encapsulate the semantics of specific groundwater data models.
Core VVG library; must load before app.js
Depends on: lib_bore.js, OpenLayers.js

@author: Armin Schmidt, A.Schmidt@GeodataWIZ.com
@version: 1.6.3
@date: 24 May 2012

@history: V1.6.3 - 24/05/2012 - KC: See Google Code for details
@history: V1.6 - 13/05/2012 - Deal with missing properties of dataStoreDescriptor. Filter works with INTERSECTS, DWITHIN and BBOX.
@history: V1.5 - 11/05/2012 - Addressed problems with date conversion.
@history: V1.4 - 10/05/2012 - Distance filter moved to transect class.
@history: V1.0 - 09/04/2012 - Start
*/
var OpenLayers = OpenLayers;  // To fool JSLint despite missing module.
var debug = debug;

(function (project) {     
	project.lib = project.lib || {};
	var lib = project.lib;  // Shorthand.
	
    /* 
	==== transect ====
	A 'transect' is a line between two points. 
	The class has the following properties:
		lineString: a two-point linestring geometry running from west to east.
		units: the units of the lineString.
		mathAngle: the angle in radians between the transect and the positive x-axis (as used in calculations like tan() etc.)
		azimuth: the azimuth of the transect in degrees. Clockwise from north, hence 0deg is north, 90deg is east.
		length: the length of the transect between the two points.
	*/
	lib.transect = function (param1, param2) {
		// Class of transect.
		// If one parameter is provided it is a linestring geometry, a feature with a linestring, or a layer.
		// If two parameters are provided they are two points.
		var that = {};  // The main data object to hold all public properties and methods.
		
		// Private properties.
		// var ...
		// Private methods.
		var pointAngle = function (p1, p2) {
			// Angle from p1 to p2; mathematical in radians, i.e. 0 is positive x-axis, positive is counter-clockwise.
			return Math.atan2(p2.y-p1.y, p2.x-p1.x);
		};
		var init_fromTwoPoints = function (point1, point2) {
			// 'Constructor' from two points.
			var tempPoint = null;
			var estimatedUnits = function (point) {
				if (Math.abs(point.x) <= 360 && Math.abs(point.y) <= 360) {
					return 'degrees';
				} else {
					return 'm';
				}
			};
			// swap points if necessary: always create transect from left to right
			if (point1.x > point2.x) {
				tempPoint = point1;
				point1 = point2;
				point2 = tempPoint;
			}
			that.lineString = new OpenLayers.Geometry.LineString([point1, point2]);
			that.units = estimatedUnits(point1);
			that.mathAngle = pointAngle(point1, point2);
			that.azimuth = 90.0-that.mathAngle*180.0/Math.PI;
			if (that.azimuth < 0) {
				that.azimuth += 360.0;
			}
			that.length = point1.distanceTo(point2);
		};
		var init_fromObject = function (obj) {
			// 'Constructor' from a single object.
			var init_fromGeometry = function (geometry) {
				if (geometry.components && geometry.components.length >= 2) {
					// Use first and last point.
					init_fromTwoPoints(
						geometry.components[0], 
						geometry.components[geometry.components.length-1]
					);
				}
			};
			var init_fromFeature = function (feature) {
				init_fromGeometry(feature.geometry);
			};
			var init_fromLayer = function (layer) {
				if (layer.features.length >= 1) {
					// Since layers have a 'projection' the units could be derived from that. But an estimate will do even for these.
					init_fromFeature(layer.features[0]);  // Take the first feature.
				}
			};

			switch (obj.CLASS_NAME) {
				case 'OpenLayers.Layer.Vector':
					init_fromLayer(obj);
					break;
				case 'OpenLayers.Feature.Vector':
					init_fromFeature(obj);
					break;
				case 'OpenLayers.Geometry.LineString':
					init_fromGeometry(obj);
					break;
			}
		};
		var distanceWithNewUnits = function (distance, newUnits) {
			// Return a distance object with converted units. Allow standard OL units of
			// 'degrees' (or 'dd'), 'm', 'ft', 'km', 'mi', 'inches'.
			
			// 'distance' could have been made its own class with clone and a changeUnits method, 
			// but using object literals is easier and this function will only be used here.
				
			var toMetres = {
				// Multiply the stated unit with this number to get metres.
				degrees: 112500,  // Approximate number of metres per longitude degree in Victoria.
				m: 1, 
				ft: 0.3048,  // 12*0.0254 ; however, the american survey foot would be 1,200/3,937
				km: 1000,
				mi: 1609.344, // 5280*12*0.0254
				inches: 0.0254
			};
			var normalisedUnits = function (units) {
				var u;
				if (units === 'dd') {
					units ='degrees';
				}
				for (u in toMetres) {
					if (toMetres.hasOwnProperty(u) && units === u) {
						return units;
					}
				}
				units = 'm';  // Default value if no matching units found.
				return units;
			};
			
			var newDistance = {};
			distance.units = normalisedUnits(distance.units);
			newUnits = normalisedUnits(newUnits);
			
			newDistance.value = distance.value*toMetres[distance.units]/toMetres[newUnits];
			newDistance.units = newUnits.toLowerCase();  // Create copy.
			return newDistance;
		};
		
		// Public properties.
		that.lineString = undefined;
		that.units = 'm';
		that.mathAngle = 0;
		that.azimuth = 0;
		that.length = 0;
		// Public methods.
		that.orthogonalDistances = function (point) {
			// Return a tuple of {linearDistance, offset} for a point to the side of the transect.
			// They have the same units as the transect.
			// These calculations are planar and hence only correct in a planar projection.
			var startPoint = that.lineString.components[0];
			var distToStart = point.distanceTo(startPoint);
			var angleToStart = pointAngle(startPoint, point);
			var angleToLine = angleToStart-that.mathAngle;
			var linearDistance = distToStart*Math.cos(angleToLine);
			var offset = distToStart*Math.sin(angleToLine);  // Positive is to the left of the line (looking from the start point), negative is to the right of the line.
			return {
				linearDistance: linearDistance,
				offset: offset
			};
		};
		that.distancePolygon = function (distance) {
			// Returns a polygon around the transect with given distance. This is a 'square' buffer.
			// Units of distance and transect have to match, no check.
			var points = [],
				p1 = that.lineString.components[0],
				p2 = that.lineString.components[1];
			// Remember: cos(a+90)=-sin(a); sin(a+90)=+cos(a)
			var dx = -Math.sin(that.mathAngle)*distance.value;
			var dy = +Math.cos(that.mathAngle)*distance.value;
			
			points.push(p1);
			points.push(new OpenLayers.Geometry.Point(p1.x+dx, p1.y+dy));
			points.push(new OpenLayers.Geometry.Point(p2.x+dx, p2.y+dy));
			points.push(p2);
			points.push(new OpenLayers.Geometry.Point(p2.x-dx, p2.y-dy));
			points.push(new OpenLayers.Geometry.Point(p1.x-dx, p1.y-dy));
			points.push(p1);  // Redundant as the LinearRing is closed automatically, but necessary if only a LineString were created.
			return new OpenLayers.Geometry.Polygon(
				[new OpenLayers.Geometry.LinearRing(points)]
			);
		};
			
		that.distanceFilter = function (distance, serverParams) {
			// Return a distance filter (i.e. buffer) for the transect, 
			// using serverParams.bestDistanceFilter = OpenLayers.Filter.Spatial.<filter>:
			// INTERSECT: all features that fall into the buffer polygon.
			// DWITHIN: all features that are within 'distance' from the transect. Possible inaccuracies when converting between degrees and metres.
			// BBOX: all features within the BBOX around the buffer polygon.
			
			serverParams.units = serverParams.units || 'degrees';  // If no units provided, likely that it is in WGS84, i.e. degrees.
			// Estimate the projection of the transect.
			var transectSRSname;
			if (that.units === 'm') {
				transectSRSname = 'EPSG:900913';
			} else {
				transectSRSname = 'EPSG:4326';
			}
			// Express the distance also in units of the transect and the server.
			var distanceTransect = distanceWithNewUnits(distance, that.units);
			var distanceServer = distanceWithNewUnits(distance, serverParams.units);
			var olFilter,
				polyBuffer;

			switch (serverParams.bestDistanceFilter) {
				case OpenLayers.Filter.Spatial.INTERSECTS:
					polyBuffer = that.distancePolygon(distanceTransect);
					olFilter = new OpenLayers.Filter.Spatial({
						type: OpenLayers.Filter.Spatial.INTERSECTS,
						projection: transectSRSname,  // The projection of the polygon, so that the server can convert it to its own units.
						value: polyBuffer
					});
					break;
				case OpenLayers.Filter.Spatial.DWITHIN:
					olFilter = new OpenLayers.Filter.Spatial({
						type: OpenLayers.Filter.Spatial.DWITHIN,
						distance: distanceServer.value,
						distanceUnits: serverParams.units,
						projection: transectSRSname,  // The projection of the lineString, so that the server can convert it to its own units.
						value: that.lineString
					});
					break;
				case OpenLayers.Filter.Spatial.BBOX:
					polyBuffer = that.distancePolygon(distanceTransect);
					polyBuffer.calculateBounds();
					olFilter = new OpenLayers.Filter.Spatial({
						type: OpenLayers.Filter.Spatial.BBOX,
						projection: transectSRSname,  // The projection of the polygon, so that the server can convert it to its own units.
						value: polyBuffer.bounds
					});
					break;
			}
			return olFilter;
		};
      
      that.distanceWithNewUnits = distanceWithNewUnits;
      that.normaliseDistance = function(distance) {
         return distanceWithNewUnits(distance, that.units);
      };
		
		if (param2) {
			init_fromTwoPoints(param1, param2);
		} else {
			init_fromObject(param1);
		}
		return that;
	};

	/*
	==== dataStore ====
	The 'dataStore' class encapsulates all the information known about data served by a WFS,
	which is composed of knowledge of the database schema, what it means and how it is
	interpreted, especially as a 'boreTransect'.
	properties: none
	methods:
		layerAll(): Return a new WMS layer that represents all data in the WFS.
		requestTransect(transect, distance, callback): Request data from the WFS along the transect.
		requestHasCompleted(): Return whether the request for the transect data has completed.
		requestDuration(): Return the duration in milliseconds required for the completion of the request.
		fillLayerFromTransect(layer, params): Add the features from the previously requested transect to the already existing vector layer.
		getLayerFromTransect(params): Return a new vector layer filled with the features from the previously requested transect.
		boreTransect(): Return a new boreTransect instance from the previously requested transect.
	*/
	lib.dataStore = function (dataStoreDescriptor) {
		/*
		dataStoreDescriptor has the following properties:
			parametersWMS
				name
				url
				params
			parametersWFS
				name
				serverParams
					bestDistanceFilter
					units
				params  // includes .url
			boreDescriptor
			featureToBore: function (feature) {
				...
				return bore;
			}
		*/
		var that = {};  // The main data object to hold all public properties and methods.
		
		// Private properties.
      var protocol = null; // OpenLayers WFS Protocol object, created in init()
		var transectRequestState = {
			response: {},  // Holds the transect features returned from the WFS request.
			transect: {},  // Transect used in the request.
			distance: {},  // Distance used in the request.
			startTimeMilliSec: 0,
			callbackFunction: {},
			callbackParams: {
				duration: 0,
				hasCompleted: false,
				hasErrors: false  // Error checking not yet implemented but parameter is used. @@
			}
		};  
		// Private methods.
		var init = function (dataStoreDescriptor) {
			// 'Constructor'.
			// Handle missing properties in dataStoreDescriptor.
			if (!dataStoreDescriptor.parametersWMS || !dataStoreDescriptor.parametersWMS.url) {
				dataStoreDescriptor.parametersWMS = {
					name: 'OSGeo vmap0',
					url: 'http://vmap0.tiles.osgeo.org/wms/vmap0?', 
					params: {
						layers: 'basic',
						transparent: true,
						isBaseLayer: false,
						singleTile: true,
						opacity: 0.6,
						visibility: true
					}
				};
			}
			if (!dataStoreDescriptor.parametersWFS || !dataStoreDescriptor.parametersWFS.params) {
				dataStoreDescriptor.parametersWFS = {
					params: {
						// Use any demo URL. See http://geoserver.org/display/GEOS/Available+WMS+and+WFS+servers
						url: 'http://www2.dmsolutions.ca/cgi-bin/mswfs_gmap?version=1.0.0&request=getcapabilities&service=wfs'
					}
				};
			}
			if (!dataStoreDescriptor.parametersWFS.serverParams) {
				dataStoreDescriptor.parametersWFS.serverParams = {
					bestDistanceFilter: OpenLayers.Filter.Spatial.DWITHIN,  // GetCapabilities is not reliable, especially if a SISS implementation is used.
					units: 'degrees'  // Could, in theory, be obtained from the WFS with a GetCapabilities. Hardcoded here, for performance.
				};
			}
			// No check for dataStoreDescriptor.boreDescriptor, as no reasonable defaults possible.
			if (!dataStoreDescriptor.featureToBore) {
				dataStoreDescriptor.featureToBore = function (feature) {
					return feature.attributes;
				};
			}
         
         protocol = new OpenLayers.Protocol.WFS(dataStoreDescriptor.parametersWFS.params);
		};
		var transectRequestCompleted = function () {
			// Called when the transect request has completed. 
			// Calls the external callback function with parameters 'transectRequestState.callbackParams'.
			
			var createUpdatedFeatures = function (features, transect, distance) {
				// Remove those features that are beyond the distance from the transect
				// and add 'linearDistance' and 'offset' to the attributes of the others.
				var initialLength = features.length;
            var finalLength = 0;
            var updatedFeatures = [];
				var i = 0, 
					feature,
					point, 
					orthogonalDistances;
				if (transect && distance) {
					for(i = 0; i < initialLength; i++) {
						feature = features[i];
						point = feature.geometry;  // Assumed to be a point!
						orthogonalDistances = transect.orthogonalDistances(point);
						if (
							orthogonalDistances.linearDistance >= 0 &&
							orthogonalDistances.linearDistance <= transect.length &&
							Math.abs(orthogonalDistances.offset) <= transect.normaliseDistance(distance).value
						) {
							feature.attributes.linearDistance = orthogonalDistances.linearDistance;
							feature.attributes.offset = orthogonalDistances.offset;
                     updatedFeatures.push(feature);
						} else {
							// Destroy the feature to free  memory.
							feature.destroy();
						}
					}
               finalLength = updatedFeatures.length;
				}
				if (debug) {
               alert(
                  'Removed '+(initialLength-finalLength)+
                  ' outliers to reduce features from '+
                  initialLength+' to '+finalLength
               );
            }
            return updatedFeatures;
			};
			
			transectRequestState.callbackParams.duration = +new Date()-transectRequestState.startTimeMilliSec;
			transectRequestState.callbackParams.hasCompleted = true;
			// Now check for errors in transectRequestState.response. Not yet implemented ... @@
			if (!transectRequestState.callbackParams.hasErrors) {
            transectRequestState.response.features = createUpdatedFeatures(
					transectRequestState.response.features,
					transectRequestState.transect,
					transectRequestState.distance
				);
			}
			// Called even in case of erros so that a waiting function doesn't hang.
			transectRequestState.callbackFunction(transectRequestState.callbackParams);
		};
		
		// Public methods.
		that.getDataStoreDescriptor = function () {
			// Return the dataStoreDescriptor object that had been provided 
			// when the class was instantiated.
			return dataStoreDescriptor;
		};
		that.layerAll = function () {
			// Return a new WMS layer that represents all data in the WFS.
			if (dataStoreDescriptor.parametersWMS.url) {
				return new OpenLayers.Layer.WMS(
					dataStoreDescriptor.parametersWMS.name,
					dataStoreDescriptor.parametersWMS.url,
					dataStoreDescriptor.parametersWMS.params
				);
			} else {
				return new OpenLayers.Layer.Vector('empty');
			}
		};
		that.reload = function (transect, distance, callback) {
//		that.requestTransect = function (transect, distance, callback) {
			// Request data from the WFS along the transect.
			// transect = class{linestring, mathAngle, azimuth, length}; see above
			// distance = {value, units}
			// callback = function (): function to be called when the request has completed.
			
			protocol.defaultFilter = transect.distanceFilter(
            distance, dataStoreDescriptor.parametersWFS.serverParams);
			
			transectRequestState = {
				transect: transect,
				distance: distance,
				startTimeMilliSec: +new Date(),
				callbackFunction: callback || function () {},
				callbackParams: {
					duration: 0,
					hasCompleted: false,
					hasErrors: false  // Error checking not yet implemented but parameter is used. @@
				},
				response: protocol.read({
					callback: transectRequestCompleted
				})  // Start the WFS query.
			};
		};
		that.requestHasCompleted = function () {
			// Return whether the request for the transect data has completed.
			return transectRequestState.callbackParams.hasCompleted;
		};
		that.requestDuration = function () {
			// Return the duration in milliseconds required for the completion of the request.
			return transectRequestState.callbackParams.duration;
		};
		that.fillLayerFromTransect = function (layer, params) {
			// Add the features from the previously requested transect to the already existing vector layer.
			// If params are provided with a property simplifyByAttribute then this attribute
			// will be made unique before the features are added to the layer.
			// For example if 'label' is chosen then duplicate features with the same label are removed.
			
			var simplifiedFeatures = function (features, attribute) {
				// Crude simplification to remove duplicates in next-index features with the same attribue.
				// Works well with bore_id as this is usually the sort criterium of the table.
				var i,
					feature,
					lastAttributeValue = '',
					sFeatures = [];
				for (i = 0; i < features.length; i += 1) {
					feature = features[i];
					if (feature.attributes[attribute] !== lastAttributeValue) {
						sFeatures.push(feature);
						lastAttributeValue = feature.attributes[attribute];
					}
				}
				if (debug) {alert('Reduced features from '+features.length+' to '+sFeatures.length);}
				return sFeatures;
			};
				
			if (
				transectRequestState.callbackParams.hasCompleted &&
				!transectRequestState.callbackParams.hasErrors
			) {
				if (params && params.simplifyByAttribute) {
					layer.addFeatures(simplifiedFeatures(
						transectRequestState.response.features, 
						params.simplifyByAttribute
					));
				} else {
					layer.addFeatures(transectRequestState.response.features);
				}
			}
		};
		that.getLayerFromTransect = function (params) {
			// Return a new vector layer filled with the features from the previously requested transect.
			// For params see fillLayerFromTransect().
			var layer = new OpenLayers.Layer.Vector(dataStoreDescriptor.parametersWFS.name);
			that.fillLayerFromTransect(layer, params);
			return layer;
		};
      that.allFeatures = function() {
			if(transectRequestState.callbackParams.hasCompleted &&
				!transectRequestState.callbackParams.hasErrors) {
				return transectRequestState.response.features.slice(0);
         } else {
            return [];
         }
      };
		that.boreTransect = function () {
			// Return a new boreTransect instance from the previously requested transect.
			var i, 
				feature, 
				bore;
			var thisBoreTransect = lib.bore.boreTransect(dataStoreDescriptor.boreDescriptor);
			if (
				transectRequestState.callbackParams.hasCompleted &&
				!transectRequestState.callbackParams.hasErrors
			) {
				for (i = 0; i < transectRequestState.response.features.length; i += 1) {
					feature = transectRequestState.response.features[i];
					bore = dataStoreDescriptor.featureToBore(feature);
					thisBoreTransect.addBore(bore);
				}
			}
			return thisBoreTransect;
		};
		
		init(dataStoreDescriptor);
		return that;
	};
	
	/*
	==== Instances of dataStore ====
	*/
	var dataCategories = lib.bore.dataCategories;  // Shorthand.
	// Bores with aquifers and waterlevels.
	lib.dataStoreAqWl = lib.dataStore({
		parametersWMS: {
			name: 'DPI-Vic bores WMS',
			url: 'http://siss.versi.edu.au/geoserver/wms?service=WMS',
			params: {
				layers: 'mv_vvg_bores_aq_wl', 
				transparent: true,
				isBaseLayer: false,
				singleTile: false,
				opacity: 0.6,
				visibility: true
			}
		},
		parametersWFS: {
			// This WFS featureType (i.e. table) returns the following attributes:
			// bore_id, site_id, geom_loc, rlns, bdepth, case_height, 
			// aquifername, from_depth, to_depth, date, depth, reduced_level, natsurf_level, 
			name: 'DPI-Vic bores WFS',
			serverParams: {
				// GetCapabilities is not reliable, especially if a SISS implementation is used.
				bestDistanceFilter: OpenLayers.Filter.Spatial.INTERSECTS,
				//bestDistanceFilter: OpenLayers.Filter.Spatial.DWITHIN,  
				//bestDistanceFilter: OpenLayers.Filter.Spatial.BBOX,
				units: 'degrees'  // Could, in theory, be obtained from the WFS with a GetCapabilities. Hardcoded here, for performance.
			},
			params: {
				url: 'http://siss.versi.edu.au/geoserver/wfs?service=WFS',
				featureType: 'mv_vvg_bores_aq_wl',
				geometryName: 'geom_loc',
				extractAttributes : true,
				srsName: 'EPSG:900913',  // SRS of the map.
				version: '1.1.0'
			}
		},
		boreDescriptor: {
			label: {
				dataCategory: dataCategories.DESCRIPTOR,
				attemptInterpolate: false,
				defaultValue: 'Bore ?'
			},
			linearDistance: {
				dataCategory: dataCategories.DESCRIPTOR
			},
			offset: {
				dataCategory: dataCategories.DESCRIPTOR
			},
			elevation: {
				dataCategory: dataCategories.DESCRIPTOR,
				zeroAllowed: false,
				defaultValue: 100.0
			},
			boreDepth: {
				dataCategory: dataCategories.DESCRIPTOR
			},
			groundwaterLevel: {
				dataCategory: dataCategories.LEVEL,
				interfaceIndex: 0,
				style: 'blue' // Or anything else that allows to style the level.
			},
			groundwaterLevelDate: {
				dataCategory: dataCategories.DATE,
				relatesTo: 'groundwaterLevel',
				attemptInterpolate: false,
				zeroAllowed: false,
				defaultValue: '1970-01-01T00:00:00+10:00',
				dateFormat: undefined
				// If 'boreDescriptor[key].dateFormat' is provided, parsing is done with
				// date.js by Matt Kruse http://www.mattkruse.com/javascript/date/
				// Mismatches between input and format are not tolerated.
				// If no 'dateFormat' is given JavaScript's 'Date.parse()' is used.
				// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date/parse
			},
			aquiferTopLevel: {
				dataCategory: dataCategories.LEVEL
			},
			aquiferBottomLevel: {
				dataCategory: dataCategories.LEVEL
			}
		},
		featureToBore: function (feature) {
			// Fields of table mv_vvg_bores_aq_wl:
			// bore_id, site_id, geom_loc, rlns, bdepth, case_height, 
			// aquifername, from_depth, to_depth, date, depth, reduced_level, natsurf_level.
			var bore_id = feature.attributes.bore_id;
			var rlns = +feature.attributes.rlns;
			var bdepth = +feature.attributes.bdepth;
			var case_height = +feature.attributes.case_height;
			var from_depth = +feature.attributes.from_depth;
			var to_depth = +feature.attributes.to_depth;
			var date = feature.attributes.date;
			var depth = +feature.attributes.depth;
			// Automatically added.
			var linearDistance = feature.attributes.linearDistance;
			var offset = feature.attributes.offset;
			
			var aqTop, aqBot;
			if (from_depth) {
				aqTop = rlns-from_depth;
			} else {
				aqTop = rlns;
			}
			if (to_depth) {
				aqBot = rlns-to_depth;
			} else {
				aqBot = rlns-bdepth;  // Assume depth of bore if no aquifer bottom defined.
			}

			var bore = {
				label: 'Bore '+bore_id,
				linearDistance: linearDistance,
				offset: offset,
				elevation: rlns,
				boreDepth: bdepth,
				groundwaterLevel: rlns-(depth-case_height),
				groundwaterLevelDate: date,
				aquiferTopLevel: aqTop,
				aquiferBottomLevel: aqBot
			};
			return bore;
		}
	});
	// Bores with waterlevels.
	lib.dataStoreWl = lib.dataStore({
		parametersWMS: {
			name: 'DPI-Vic bores WMS',
			url: 'http://siss.versi.edu.au/geoserver/wms?service=WMS',
			params: {
				layers: 'mv_vvg_bores_wl', 
				transparent: true,
				isBaseLayer: false,
				singleTile: false,
				opacity: 0.6,
				visibility: true
			}
		},
		parametersWFS: {
			// This WFS featureType (i.e. table) returns the following attributes:
			// bore_id, site_id, geom_loc, rlns, bdepth, case_height, 
			// date, depth, reduced_level, natsurf_level
			name: 'DPI-Vic bores WFS',
			serverParams: {
				bestDistanceFilter: OpenLayers.Filter.Spatial.DWITHIN,  // GetCapabilities is not reliable, especially if a SISS implementation is used.
				units: 'degrees'  // Could, in theory, be obtained from the WFS with a GetCapabilities. Hardcoded here, for performance.
			},
			params: {
				url: 'http://siss.versi.edu.au/geoserver/wfs?service=WFS',
				featureType: 'mv_vvg_bores_wl',
				geometryName: 'geom_loc',
				extractAttributes : true,
				srsName: 'EPSG:900913',  // SRS of the map.
				version: '1.1.0'
			}
		},
		boreDescriptor: {
			label: {
				dataCategory: dataCategories.DESCRIPTOR,
				attemptInterpolate: false,
				defaultValue: 'Bore ?'
			},
			linearDistance: {
				dataCategory: dataCategories.DESCRIPTOR
			},
			offset: {
				dataCategory: dataCategories.DESCRIPTOR
			},
			elevation: {
				dataCategory: dataCategories.DESCRIPTOR,
				zeroAllowed: false,
				defaultValue: 100.0
			},
			boreDepth: {
				dataCategory: dataCategories.DESCRIPTOR
			},
			groundwaterLevel: {
				dataCategory: dataCategories.LEVEL,
				interfaceIndex: 0,
				style: 'blue' // Or anything else that allows to style the level.
			},
			groundwaterLevelDate: {
				dataCategory: dataCategories.DATE,
				relatesTo: 'groundwaterLevel',
				attemptInterpolate: false,
				zeroAllowed: false,
				defaultValue: '1970-01-01T00:00:00+10:00',
				dateFormat: undefined
				// If 'boreDescriptor[key].dateFormat' is provided, parsing is done with
				// date.js by Matt Kruse http://www.mattkruse.com/javascript/date/
				// Mismatches between input and format are not tolerated.
				// If no 'dateFormat' is given JavaScript's 'Date.parse()' is used.
				// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date/parse
			}
		},
		featureToBore: function (feature) {
			// Fields of table mv_vvg_bores_aq_wl:
			// bore_id, site_id, geom_loc, rlns, bdepth, case_height, 
			// date, depth, reduced_level, natsurf_level
			var bore_id = feature.attributes.bore_id;
			var rlns = +feature.attributes.rlns;
			var bdepth = +feature.attributes.bdepth;
			var case_height = +feature.attributes.case_height;
			var date = feature.attributes.date;
			var depth = +feature.attributes.depth;
			// Automatically added.
			var linearDistance = feature.attributes.linearDistance;
			var offset = feature.attributes.offset;
			
			var bore = {
				label: 'Bore '+bore_id,
				linearDistance: linearDistance,
				offset: offset,
				elevation: rlns,
				boreDepth: bdepth,
				groundwaterLevel: rlns-(depth-case_height),
				groundwaterLevelDate: date
			};
			return bore;
		}
	});
	// Bores with aquifers.
	lib.dataStoreAq = lib.dataStore({
		parametersWMS: {
			name: 'DPI-Vic bores WMS',
			url: 'http://siss.versi.edu.au/geoserver/wms?service=WMS',
			params: {
				layers: 'mv_vvg_bores_aq', 
				transparent: true,
				isBaseLayer: false,
				singleTile: false,
				opacity: 0.6,
				visibility: true
			}
		},
		parametersWFS: {
			// This WFS featureType (i.e. table) returns the following attributes:
			// bore_id, site_id, gml_id, sp_gml_id, bc_gml_id, survey_basis, positionAccuracy, 
			// geom_loc, bdepth, bowner, driller, drilled_date, diameter, rlns, case_height, 
			// gwml_method, aquifername, from_depth, to_depth
			name: 'DPI-Vic bores WFS',
			serverParams: {
				bestDistanceFilter: OpenLayers.Filter.Spatial.DWITHIN,  // GetCapabilities is not reliable, especially if a SISS implementation is used.
				units: 'degrees'  // Could, in theory, be obtained from the WFS with a GetCapabilities. Hardcoded here, for performance.
			},
			params: {
				url: 'http://siss.versi.edu.au/geoserver/wfs?service=WFS',
				featureType: 'mv_vvg_bores_aq',
				geometryName: 'geom_loc',
				extractAttributes : true,
				srsName: 'EPSG:900913',  // SRS of the map.
				version: '1.1.0'
			}
		},
		boreDescriptor: {
			label: {
				dataCategory: dataCategories.DESCRIPTOR,
				attemptInterpolate: false,
				defaultValue: 'Bore ?'
			},
			linearDistance: {
				dataCategory: dataCategories.DESCRIPTOR
			},
			offset: {
				dataCategory: dataCategories.DESCRIPTOR
			},
			elevation: {
				dataCategory: dataCategories.DESCRIPTOR,
				zeroAllowed: false,
				defaultValue: 100.0
			},
			boreDepth: {
				dataCategory: dataCategories.DESCRIPTOR
			},
			aquiferTopLevel: {
				dataCategory: dataCategories.LEVEL
			},
			aquiferBottomLevel: {
				dataCategory: dataCategories.LEVEL
			}
		},
		featureToBore: function (feature) {
			// Fields of table mv_vvg_bores_aq:
			// bore_id, site_id, gml_id, sp_gml_id, bc_gml_id, survey_basis, positionAccuracy, 
			// geom_loc, bdepth, bowner, driller, drilled_date, diameter, rlns, case_height, 
			// gwml_method, aquifername, from_depth, to_depth
			var bore_id = feature.attributes.bore_id;
			var rlns = +feature.attributes.rlns;
			var bdepth = +feature.attributes.bdepth;
			var from_depth = +feature.attributes.from_depth;
			var to_depth = +feature.attributes.to_depth;
			// Automatically added.
			var linearDistance = feature.attributes.linearDistance;
			var offset = feature.attributes.offset;
			
			var aqTop, aqBot;
			if (from_depth) {
				aqTop = rlns-from_depth;
			} else {
				aqTop = rlns;
			}
			if (to_depth) {
				aqBot = rlns-to_depth;
			} else {
				aqBot = rlns-bdepth;  // Assume depth of bore if no aquifer bottom defined.
			}

			var bore = {
				label: 'Bore '+bore_id,
				linearDistance: linearDistance,
				offset: offset,
				elevation: rlns,
				boreDepth: bdepth,
				aquiferTopLevel: aqTop,
				aquiferBottomLevel: aqBot
			};
			return bore;
		}
	});
}(
window.vvg = window.vvg || {}
));
